-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/#1032 working veto system #1033
base: develop
Are you sure you want to change the base?
Feature/#1032 working veto system #1033
Conversation
if (tokensTotalPerPlayer[player.id] > veto_tokens_per_player): | ||
raise RuntimeError(f"Player {player.id} has too many vetoes!") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not do validation of the number of tokens per player when starting the game. This validation and error raising should happen when the server receives the set_vetoes message. This is because as it is now a player could send the server more than the max amount of vetoes and then cause a game to never be able to launch.
def calculate_dynamic_tokens_per_map(self, M: float, tokens: list[int]) -> float: | ||
sorted_tokens = sorted(tokens) | ||
if (sorted_tokens.count(0) >= M): | ||
return 1 | ||
|
||
result = 1; last = 0; index = 0 | ||
while (index < len(sorted_tokens)): | ||
(index, last) = next(((i, el) for i, el in enumerate(sorted_tokens) if el > last), (len(sorted_tokens) - 1, sorted_tokens[-1])) | ||
index += 1 | ||
divider = index - M | ||
if (divider <= 0): | ||
continue | ||
|
||
result = sum(sorted_tokens[:index]) / divider | ||
upperLimit = sorted_tokens[index] if index < len(sorted_tokens) else float('inf') | ||
if (result <= upperLimit): | ||
return result | ||
|
||
raise Exception("Failed to calculate dynamic tokens per map: impossible vetoes setup") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly here we are throwing an exception based on user input so a user could craft a veto selection that could cause the majority of games to fail to start. It would be better to have some default value rather than throwing an exception.
also idk how least_common should work |
75a18d1
to
0a667d2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only wanted to ask what calculate_dynamic_tokens_per_map
function is doing, because i don't fully comprehend it from the code, but also left some other comments/suggestions.
I am not a maintainer of this repo, therefore feel free to ignore them or use them at your own risk
@@ -523,7 +542,23 @@ def get_displayed_rating(player: Player) -> float: | |||
pool = queue.get_map_pool_for_rating(rating) | |||
if not pool: | |||
raise RuntimeError(f"No map pool available for rating {rating}!") | |||
game_map = pool.choose_map(played_map_ids) | |||
|
|||
pool, _, _, _, max_tokens_per_map, minimum_maps_after_veto = queue.map_pools[pool.id] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use *_
instead of 3 _
to unpack unneeded variables
|
||
pool, _, _, _, max_tokens_per_map, minimum_maps_after_veto = queue.map_pools[pool.id] | ||
|
||
vetoesMap = defaultdict(int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it suddenly in camelCase?
vetoesMap[m.map_pool_map_version_id] += player.vetoes.get(m.map_pool_map_version_id, 0) | ||
|
||
if (max_tokens_per_map == 0): | ||
max_tokens_per_map = self.calculate_dynamic_tokens_per_map(minimum_maps_after_veto, vetoesMap.values()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calculate_dynamic_tokens_per_map
expects list[int]
, but vetoesMap.values()
are actually not a list. if you want to annotate dict's values, you can use ValuesView
from collections.abc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good notice
i'll replace calculate_dynamic_tokens_per_map tokens type with Iterable[int]
@@ -673,6 +708,35 @@ async def launch_match( | |||
if player not in connected_players | |||
]) | |||
|
|||
def calculate_dynamic_tokens_per_map(self, M: float, tokens: list[int]) -> float: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you trying to achieve sum(weights) = M
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calculate_dynamic_tokens_per_map finds the minimal possible max_tokens_per_map while still respecting M
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but when do you decide that max_tokens_per_map
can't be reduced anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its just some math
it solves series of equations, if vetoes set correctly, one of them is guaranteed to be correct answer
if vetoes set incorrectly, it will return 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'll prepare some explanation how math works
@@ -78,12 +78,15 @@ def add_map_pool( | |||
self, | |||
map_pool: MapPool, | |||
min_rating: Optional[int], | |||
max_rating: Optional[int] | |||
max_rating: Optional[int], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are all of those arguments not inside MapPool
🤔 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because its not in the mapPool in the database, but in matchmaker_queue_map_pool (queue bracket), so should be consistent between repos
and yea its that way in db due to some reasons which were discussed long ago (f.e. u can use same pool for multiple brackets)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
server/players.py
Outdated
raise ValueError("Incorrect vetoes dictonary") | ||
self._vetoes = value | ||
|
||
async def update_vetoes(self, pools_vetodata: list[tuple[list[int], int, int]], current: dict = None) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could create a container (NamedTuple) for this tuple[int], int, int
structure and avoid those long type hints and maybe call it MapPoolVeto
also, if you put tokens and ratings information in MapPool
(in database they are in the same table, why are they separated here?) you won't even need this additional structure and you will be able to pass MapPool
and use its properties
finally, this can also possibly remove get_pools_veto_data
method since it just rearranges MapPool
's internals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vetoes are not in map pool in database https://faforever.github.io/db/tables/map_pool.html
vetoes are here: https://faforever.github.io/db/tables/matchmaker_queue_map_pool.html
the same place where min and max rating, outside of map pool itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also i am not sure wdym removing get_pools_veto_data
it gives only necessary data and readable name for it, allows to avoid any code duplication and keeps the code more clean
i am using it in multiple places so if we remove it code will become just worse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, i see
What i mean is that now matchmaker_queue_map_pool
has additional 3 new fields and it may be viable to create corresponding data container (see, for example, FeaturedMod
).
This way you preserve readable names and also make it easier to understand what's passed to other functions
For example, in my opinion
async def update_vetoes(self, list[MatchmakerQueueMapPool], current: dict | None) -> None
is easier to follow than to find calls to update_vetoes
and discover what's inside of list[tuple[list[int], int, int]]
also,
map_pools: Iterable[MatchmakerQueueMapPool]
is easier to understand than
map_pools: Iterable[tuple[MapPool, Optional[int], Optional[int], int, int, float]] = ()
where MatchmakerQueueMapPool
is
class MatchmakerQueueMapPool(NamedTuple):
map_pool: MapPool
min_rating: int | None
max_rating: int | None
veto_tokens_per_player: int
max_tokens_per_map: int
minimum_maps_after_veto: float
finally, this way long unpacking can be avoided and instead of
for pool, *_, veto_tokens_per_player, max_tokens_per_map, _ in queue.map_pools.values():
you can easily access named attributes
for pool in queue.map_pools.values():
pool.veto_tokens_per_player...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, yea, but this going into refactoring existing code zone rather than implementing new features so i was not sure should i do it here or not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using and changing to a NamedTuple should be fine and probably preferred as Gatsik says
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cannot put named tuple MatchmakerQueueMapPool right inside types.py due to circular dependencies happening (map_pool.py uses types and types uses map_pool.py in this case)
is it ok no place it right inside map_pool.py?
server/matchmaker/map_pool.py
Outdated
map_id = random.choices(least_common, weights=weights, k=1)[0][0] | ||
return self.maps[map_id].get_map() | ||
# Multiply weight by 2 if map is least common and not vetoed by anyone | ||
mapList = list((map.map_pool_map_version_id, map, 2 if (map.id in least_common_ids) and (vetoesMap.get(map.map_pool_map_version_id, 0) == 0) else 1) for id, map in self.maps.items()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't you feel like this line is a bit long?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
long lines for long PP
i'll add some improvements soon |
ok not soon but tomorrow ;) |
4) otherwise | ||
solving equation for current group | ||
and checking the result vs upper border | ||
and upper border is equal to the amount of tokens applied to the map next to last map in our group, or infinity if there is no such one |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the explanation could be shorter
Lets denote max_tokens_per_map
= len(tokens)
=
If some
Given sorted array, excluding the last element
This excluding won't help if
Lets denote
But
This means, that we can stop trying to decrease
And you can describte the algorithm as easy as:
- Find
$d = \frac{\sum_{i=1}^{n}a_{i}}{n - M}$ , if$d > \max(tokens)$ then$d$ is found. otherwise exclude all$a_{i} \geq d$ . Repeat while$n > M$
(And of course handle special case when
solving equation for current group | ||
and checking the result vs upper border | ||
and upper border is equal to the amount of tokens applied to the map next to last map in our group, or infinity if there is no such one | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe put docstring inside the function definition?
minimum_maps_after_veto = 1 | ||
self._logger.error(f"Wrong vetoes setup detected for pool {pool.id} in queue {queue.id}") | ||
result.append( | ||
MatchmakerQueueMapPoolVetoData( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't really understand why you need MatchmakerQueueMapPoolVetoData
. Why don't you just initialize MatchmakerQueueMapPool
with correct vetoes setup and use it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
atleast to avoid having any possible issues with queue.map_pools.clear() in update_data
theoretically user can change vetoes mid-queues-update, then server will delete all user's vetoes and he will need to apply all of them again which is very bad
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Askaholic said "Technically we do run the matchmaking algorithm in a separate thread because it is the one piece of the code that has the potential to block the rest of the server if there are a lot of players in queue."
So i prefer to have robust solution here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that statement is relevant to vetos because the 'chose_map' stage happens after the matchmaking algorithm has finished. But you should probably set it up in such a way that vetos are locked in the moment a match is found like we do with factions in the PlayerParty.on_matched
function. I think keeping the behavior for choosing vetos and factions consistent makes the most sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that statement is relevant to vetos because the 'chose_map' stage happens after the matchmaking algorithm has finished. But you should probably set it up in such a way that vetos are locked in the moment a match is found like we do with factions in the
PlayerParty.on_matched
function. I think keeping the behavior for choosing vetos and factions consistent makes the most sense.
its not about chose_map, it about command_set_player_vetoes in lobbyconection.py
it uses matchmaker map pool data and this is async function, and i am not 100% sure how this works here
also whats the point of locking vetoes when match found? what can possibly go wrong if we dont do that?
|
||
class MatchmakerQueueMapPool(NamedTuple): | ||
map_pool: MapPool | ||
min_rating: int | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use Optional[int]
, because looks like server hasn't adopted this syntax yet
Working on #1032
I need some feedback, what is right, what is wrong, what needs to be changed, etc